package com.zzh.interview.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zzh.interview.common.ErrorCode;
import com.zzh.interview.constant.RedisConstant;
import com.zzh.interview.exception.BusinessException;
import com.zzh.interview.mapper.UserMapper;
import com.zzh.interview.model.dto.user.UserTDO;
import com.zzh.interview.model.entity.User;
import com.zzh.interview.model.enums.UserRoleEnum;
import com.zzh.interview.model.vo.UserVO;
import com.zzh.interview.satoken.utils.DeviceUtils;
import com.zzh.interview.service.UserService;

import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.Year;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import com.zzh.interview.utils.UserAccountUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RBitSet;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import static com.zzh.interview.constant.UserConstant.*;

/**
 * 用户服务实现
 *
 * @author <a href="https://github.com/liyupi">程序员鱼皮</a>
 * @from <a href="https://yupi.icu">编程导航知识星球</a>
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private UserMapper userMapper;

    //    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedisTemplate<String,Object> redisStringTemplate;

    @Resource
    private RedissonClient redissonClient;


    /**
     * 用户注册(邮箱形式) =>成功返回 新的账户 和 id
     * @param  email 邮箱 + 验证码 + 密码
     * @param  password 邮箱 + 验证码 + 密码
     * @param  code 邮箱 + 验证码 + 密码
     * @return
     */
    @Override
    public String userRegisterEmail(String email , String password , String code){
        //1.判断code是否正确
        String codeRedis = (String) redisStringTemplate.opsForValue().get("email" + email);
        if (codeRedis == null || !code.equals(codeRedis)){
            throw new BusinessException(40001,"验证码不正确");
        }

        // 2. 密码加密
        //使用Spring的加密方法，采用MD5加密方式
        String encryptPassword = DigestUtils.md5DigestAsHex((USER_PASSWORD_PREFIX + password).getBytes(StandardCharsets.UTF_8));

        //避免多次插入数据
        RLock lock = redissonClient.getLock(USER_LOCK_REGISTER_EMAIL_KEY + email);
        try {
            //1.保证只能一个线程拿到锁
            if(lock.tryLock(0,30000L, TimeUnit.MILLISECONDS)){
                //检查我们的email是否已经注册过了
                //select id from user where email='';
                User userEmail = baseMapper.selectOne(new QueryWrapper<User>().eq("email", email).select("id"));
                if (userEmail != null){//存在user =》email要做为索引
                    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
                }

                // 3. 向用户数据库插入数据
                User user = new User();

                //生成11位账户
                String account = UserAccountUtils.getUserAccountByTimeStamp();
                user.setUserAccount(account);

                user.setEmail(email);
                user.setUserPassword(encryptPassword);
                boolean saveResult = this.save(user);//插入数据
                if (!saveResult) {
                    throw new BusinessException(ErrorCode.SYSTEM_ERROR);
                }
                return account;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //2.保证自己释放自己的锁
            if (lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }

        return null;
    }

    /**
     * 用户注册(手机号方式) =>成功返回 新的账户
     *
     * @param phone  手机号
     * @param userPassword 用户密码
     * @param code         验证码
     * @return
     */
    @Override
    public String userRegisterPhone(String phone, String userPassword, String code){
        //1.判断code是否正确
        String codeRedis = (String) redisStringTemplate.opsForValue().get(USER_LOCK_CODE_PHONE_KEY + phone);
        if (codeRedis == null || !code.equals(codeRedis)){
            throw new BusinessException(40001,"验证码不正确");
        }

        // 2. 密码加密
        //使用Spring的加密方法，采用MD5加密方式
        String encryptPassword = DigestUtils.md5DigestAsHex((USER_PASSWORD_PREFIX + userPassword).getBytes(StandardCharsets.UTF_8));

        //避免多次插入数据
        RLock lock = redissonClient.getLock(USER_LOCK_REGISTER_PHONE_KEY + phone);
        try {
            //1.保证只能一个线程拿到锁
            if(lock.tryLock(0,30000L,TimeUnit.MILLISECONDS)){
                //检查我们的email是否已经注册过了
                //select id from user where email='';
                User userEmail = baseMapper.selectOne(new QueryWrapper<User>().eq("phone", phone).select("id"));
                if (userEmail != null){//存在user =》email要做为索引
                    throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
                }

                // 3. 向用户数据库插入数据
                User user = new User();

                //todo 生成11位账户=>怎么解决更好，这里只能解决8个人同一时间的不同账户返回，超过8个可能就会出现一定的错误
                String account = UserAccountUtils.getUserAccountByTimeStamp();
                user.setUserAccount(account);

                user.setPhone(phone);
                user.setUserPassword(encryptPassword);
                boolean saveResult = this.save(user);//插入数据
                if (!saveResult) {
                    throw new BusinessException(ErrorCode.SYSTEM_ERROR);
                }
                return account;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //2.保证自己释放自己的锁
            if (lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }

        return null;
    }


    /**
     * 邮箱登录
     * @param email
     * @param code
     * @param request
     * @return
     */
    @Override
    public User userLoginEmail(String email, String code, HttpServletRequest request) {
        //2.判断验证码是否正确 —— 减少数据库开发
        String codeRedis = (String) redisStringTemplate.opsForValue().get(USER_LOCK_CODE_EMAIL_KEY + email);
        if (!code.equals(codeRedis)){
            throw new BusinessException(40001,"验证码错误");
        }
        //1.判断是否注册了！
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("email", email));
        if (user == null){
            throw new BusinessException(40001,"邮箱未注册");
        }
        // 3. 用户脱敏
        User safetyUser = getSafeUser(user);
        // 4. 记录用户登录态
//        request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);
        //使用sa-token
        // 4. 记录用户的登录态（移除这行代码）
        // Sa-Token 登录，并指定设备，同端登录互斥
        StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request));//用户id+设备信息
        // 5. 记录用户登录态（用户信息）=》到时候可以获取用户信息
        StpUtil.getSession().set(USER_LOGIN_STATE, user);

        return safetyUser;
    }

    /**
     * 手机号登录
     * @param phone
     * @param code
     * @param request
     * @return
     */
    @Override
    public User userLoginPhone(String phone, String code, HttpServletRequest request) {
        //2.判断验证码是否正确 —— 减少数据库开发
        String codeRedis = (String) redisStringTemplate.opsForValue().get(USER_LOCK_CODE_PHONE_KEY + phone);
        if (!code.equals(codeRedis)){
            throw new BusinessException(40001,"验证码错误");
        }
        //1.判断是否注册了！
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("phone", phone));
        if (user == null){
            throw new BusinessException(40001,"手机号未注册");
        }
        // 3. 用户脱敏
        User safetyUser = getSafeUser(user);
//        // 4. 记录用户登录态
//        request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);

        //使用sa-token
        // 4. 记录用户的登录态（移除这行代码）
        // Sa-Token 登录，并指定设备，同端登录互斥
        StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request));//用户id+设备信息
        // 5. 记录用户登录态（用户信息）=》到时候可以获取用户信息
        StpUtil.getSession().set(USER_LOGIN_STATE, user);
        return safetyUser;
    }


    /**
     * 用户登录
     * 测试使用
     * @param userAccount  用户账户
     * @param userPassword 用户密码
     * @param request      请求
     * @return 脱敏后的用户信息
     * @deprecated 过期
     */
    @Override
    public User userLoginAccount(String userAccount, String userPassword ,HttpServletRequest request) {
        // 1. 校验
        // 1.1 校验账户
        if (userAccount.length() != 11){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //1.2校验密码
        if (userPassword.length() < 8){
            throw new BusinessException(40001,"密码不正确");
        }

        //1.3校验验证码（前端校验）
//        String codeRedis = stringRedisTemplate.opsForValue().get(USER_LOCK_CODE_EMAIL_KEY + userAccount);
//        if (!code.equals(codeRedis)){
//            throw new BusinessException("验证码不正确",40000);
//        }

        // 2. 校验密码是否输入正确，和数据库中的密文密码去对比
        String encryptPassword = DigestUtils.md5DigestAsHex((USER_PASSWORD_PREFIX + userPassword).getBytes(StandardCharsets.UTF_8));
        System.out.println("encryptPassword"+encryptPassword);
        //查询用户是否存在且密码与之匹配
        QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("userAccount", userAccount).eq("userPassword", encryptPassword);
        String sqlSelect = wrapper.getSqlSelect();
        System.out.println("sqlSelect"+sqlSelect);
        //这里需要添加逻辑删除 以防查询到已删除的 逻辑删除使用方法：①配置yml ②在实体类字段添加@TableLogic
        User user = userMapper.selectOne(wrapper);

        if (user == null){
            throw new BusinessException(40001,"用户名或密码错误");
        }

        // 3. 用户脱敏
        User safetyUser = getSafeUser(user);

        // 4. 记录用户登录态
//        request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);

        //使用sa-token
        // 4. 记录用户的登录态（移除这行代码）
        // Sa-Token 登录，并指定设备，同端登录互斥
        StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request));//用户id+设备信息
        // 5. 记录用户登录态（用户信息）=》到时候可以获取用户信息
        StpUtil.getSession().set(USER_LOGIN_STATE, user);
        return safetyUser;

    }

    /**
     * @param username 用户名
     * @param request 请求对象
     * @return 用户列表(脱敏后的User)
     * @description 根据用户名查询用户
     */
    @Override
    public List<User> searchUsers(String username,HttpServletRequest request) {
        if (!isAdmin(request)){
            return new ArrayList<>();
        }
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(username)) {
            queryWrapper.like(User::getUserName, username);
        }
        return this.list(queryWrapper).stream().map(this::getSafeUser).collect(Collectors.toList());
    }

    /**
     * 鉴权函数：判断是否为管理员 仅管理员可查询、删除
     *
     * @param request 请求
     * @return 是否为管理员
     */
    @Override
    public boolean isAdmin(HttpServletRequest request){
        //鉴权：仅管理员可查询
//        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);

        //不从redis的session中获取，从sa-token中获取
        Object loginIdDefaultNull = StpUtil.getLoginIdDefaultNull();
        if(loginIdDefaultNull == null){
            throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
        }
        //从sa-token中获取用户信息
        User userObj = (User) StpUtil.getSession().get(USER_LOGIN_STATE);
        if (userObj == null || userObj.getUserRole() != ADMIN_ROLE){
            return false;
        }
        return true;
    }

    /**
     * 判断是否为管理员
     * @param user
     * @return
     */
    @Override
    public boolean isAdmin(User user) {
        return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());
    }


    /**
     * 用户注销退出
     * @param request
     * @return
     */
    @Override
    public boolean userLogout(HttpServletRequest request) {
//        User currentUser = getCurrentUser(request);
//        //移除Session中的登录态
//        request.getSession().removeAttribute(USER_LOGIN_STATE);

        //直接sa-token退出 先从request能拿到session=》检查是否登录过，没登录就直接抛异常NotLoginException
        StpUtil.checkLogin();
        StpUtil.logout();//登录了就退出登录

        return true;
    }


    /**
     * 用户脱敏
     *
     * @param originUser 完成用户信息
     * @return 脱敏后的用户信息
     */
    @Override
    public User getSafeUser(User originUser){
        if (originUser == null){
            return null;
        }
        User user = new User();
        user.setId(originUser.getId());
        user.setUserAccount(originUser.getUserAccount());
        user.setPhone(originUser.getPhone());
        user.setUserName(originUser.getUserName());
        user.setUserRole(originUser.getUserRole());
        user.setUserAvatar(originUser.getUserAvatar());
        user.setCreateTime(originUser.getCreateTime());
        user.setEmail(originUser.getEmail());
        user.setUserProfile(originUser.getUserProfile());
        user.setUpdateTime(originUser.getUpdateTime());
        user.setCreateTime(originUser.getCreateTime());
        user.setEditTime(originUser.getUpdateTime());
        return user;
    }


    /**
     * 更改用户信息
     * @param userTDO
     * @param currentUser
     * @return
     */
    @Override
    public boolean updateUser(UserTDO userTDO, User currentUser, HttpServletRequest request) {
        //判断当前用户是否登录
        if (userTDO == null || currentUser == null){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //判断权限
        //当前用户id 和 修改的用户信息中的id一
        Long uid = Long.parseLong(userTDO.getEditId());
        if (uid != currentUser.getId()){
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }


        //判断修改信息是否存在
        String key = userTDO.getEditKey();
        String name = userTDO.getEditName();
        String value = userTDO.getEditValue();
        String editValueNew = userTDO.getEditValueNew();
        if (StringUtils.isAllBlank(key,name,value)){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //修改 可修改有 phone , avatarUrl,email,gender,username
        if (key.equals("phone")){
            currentUser.setPhone(value);//更新session的
        }
        if (key.equals("userPassword")){
            //检查用户密码
            User user = userMapper.selectById(uid);
            //加密密码
            //使用Spring的加密方法，采用MD5加密方式
            String encryptPassword = DigestUtils.md5DigestAsHex((USER_PASSWORD_PREFIX + value).getBytes(StandardCharsets.UTF_8));
            if (!user.getUserPassword().equals(encryptPassword)) {
                throw new BusinessException(40001,"原密码不正确");
            }
            if (StringUtils.isBlank(editValueNew)){
                throw new BusinessException(ErrorCode.PARAMS_ERROR);
            }
            editValueNew = DigestUtils.md5DigestAsHex((USER_PASSWORD_PREFIX + editValueNew).getBytes(StandardCharsets.UTF_8));
            userTDO.setEditValueNew(editValueNew);
        }
        if (key.equals("email")){
            currentUser.setEmail(value);//更新session的
        }
        if (key.equals("username")){
            currentUser.setUserName(value);//更新session的
        }
        if (key.equals("personalProfile")){
            currentUser.setUserProfile(value);
        }
        int i = userMapper.updateUserInfoById(userTDO);//动态修改sql
        if (i > 0){
            //更新session
            request.getSession().setAttribute(USER_LOGIN_STATE,currentUser);
            return true;
        }
        return false;
    }


    /**
     * 获取当前用户
     * @param request
     * @return
     */
    @Override
    public User getCurrentUser(HttpServletRequest request) {
        if (request == null){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //不从redis的session中获取，从sa-token中获取
        Object loginIdDefaultNull = StpUtil.getLoginIdDefaultNull();//本质其实也是从request拿到session获取当时登录存放的id
        if(loginIdDefaultNull == null){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //redis获取用户信息
//        User currentUser = (User)request.getSession().getAttribute(USER_LOGIN_STATE);
        //登录缓存了用户信息可以直接从缓存中获取
        User currentUser = (User) StpUtil.getSession().get(USER_LOGIN_STATE);
        //或者数据库获取用户信息（根据id查询）
        // User currentUser = userMapper.selectById((String)loginIdDefaultNull);
        if (currentUser == null){
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        return currentUser;
    }



    /**
     * 异步修改用户的头像和缓存
     * @param url
     * @param request
     */
    @Override
    public void updateUserAvatarUrl(String url, HttpServletRequest request) {
        //redis修改
        User currentUser = getCurrentUser(request);
        currentUser.setUserAvatar(url);
        request.setAttribute(USER_LOGIN_STATE,currentUser);
        //数据库修改
        int update = userMapper.updateAvaTarUrl(url,currentUser.getId());
    }



    /**
     * 根据账户查询用户列表
     * @param account
     * @return
     */
    @Override
    public List<User> selectUserByAccount(String account) {
        long parseLong = Long.parseLong(account);
        List<User> userList = userMapper.selectListByAccount(parseLong);
        return userList;
    }


    @Override
    public List<User> selectUsersByIds(List<Long> idList) {
        List<User> userList = userMapper.selectBatchIds(idList);
        userList.forEach(user -> getSafeUser(user));//过滤脱敏
        return userList;
    }


    /**
     * 获取当前登录用户（允许未登录）
     * @param request
     * @return
     */
    @Override
    public User getLoginUserPermitNull(HttpServletRequest request) {
        // 先判断是否已登录（基于 Sa-Token 实现）
        Object loginUserId = StpUtil.getLoginIdDefaultNull();
        if (loginUserId == null) {
            return null;
        }
        return this.getById((String) loginUserId);
    }

    @Override
    public UserVO getUserVO(User user) {
        if (user == null) {
            return null;
        }
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(user, userVO);
        return userVO;
    }


    /**
     * 签到
     * @param userId
     * @return
     */
    @Override
    public boolean signIn(Long userId) {
        //获取当前时间的年份，然后获取到签到用到的key
        LocalDate date = LocalDate.now();
        int year = date.getYear();
        //获取到签到用到的key
        String userSignKey = RedisConstant.getUserSignKey(year, userId);
        //从redis中获取签到value
        RBitSet bitSet = redissonClient.getBitSet(userSignKey);
        int dayOfYear = date.getDayOfYear();//获取当前日期的第几天
        //判断是否签到过
        if (!bitSet.get(dayOfYear)) {
            //未签到过，则设置签到
            boolean set = bitSet.set(dayOfYear, true);
            return set;
        }
        //已经签到过
        return true;
    }

    /**
     * 获取用户签到记录,默认获取当前年份的签到记录
     * @param userId
     * @param year
     * @return
     */
    @Override
    public List<Integer> getUserSignInRecord(long userId, Integer year) {
        if (year == null) {
            LocalDate date = LocalDate.now();
            year = date.getYear();
        }
        // 获取到签到用到的key
        String key = RedisConstant.getUserSignKey(year, userId);
        // 获取到签到用到的redis bitset
        RBitSet signInBitSet = redissonClient.getBitSet(key);//获取到redis的bitset，循环则会消耗redis资源
        BitSet bitSet = signInBitSet.asBitSet();//保存到内存，减少redis的访问

        //存储已经签到过的记录内容！
        List<Integer> signInRecord = new ArrayList<>();

        // 获取当前年份的总天数
        int totalDays = Year.of(year).length();
        // 高效获取已经签到记录 (减少无谓的for获取未签到的map)
        int index = bitSet.nextSetBit(0);
        while (index >= 0){
            signInRecord.add(index);
            // 继续获取下一个已经设置的bit位
            index = bitSet.nextSetBit(index + 1);
        }
        return signInRecord;
    }


}






